/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
import {
  ArrayBindingPattern,
  ArrayLiteralExpression,
  BindingIdentifier,
  CustomExpression,
  ForOfStatement,
  Interpolation,
  ObjectBindingPattern,
  ObjectLiteralExpression,
  PrimitiveLiteralExpression,
  TaggedTemplateExpression,
  TemplateExpression,
  AnyBindingExpression,
  BinaryOperator,
  BindingIdentifierOrPattern,
  IsAssign,
  IsAssignable,
  IsBinary,
  IsBindingBehavior,
  IsExpressionOrStatement,
  IsLeftHandSide,
  IsValueConverter,
  UnaryOperator,
  DestructuringAssignmentExpression as DAE,
  type ExpressionKind,
  ekAccessThis,
  ekAccessGlobal,
  ekAccessMember,
  ekAccessScope,
  ekArrayDestructuring,
  ekArrayBindingPattern,
  ekObjectBindingPattern,
  ekBindingIdentifier,
  ekObjectDestructuring,
  AssignmentOperator,
  createAccessThisExpression,
  createAccessBoundaryExpression,
  createAccessGlobalExpression,
  createAccessScopeExpression,
  createAccessMemberExpression,
  createAccessKeyedExpression,
  createNewExpression,
  createCallScopeExpression,
  createCallMemberExpression,
  createCallFunctionExpression,
  createCallGlobalExpression,
  createUnaryExpression,
  createTemplateExpression,
  createPrimitiveLiteralExpression,
  createBinaryExpression,
  createConditionalExpression,
  createAssignExpression,
  createValueConverterExpression,
  createBindingBehaviorExpression,
  createArrayLiteralExpression,
  createObjectLiteralExpression,
  createTaggedTemplateExpression,
  createBindingIdentifier,
  createArrayBindingPattern,
  createObjectBindingPattern,
  createForOfStatement,
  createInterpolation as createInterpolationAst,
  createDestructuringAssignmentExpression,
  createDestructuringAssignmentSingleExpression,
  createArrowFunction,
  PrimitiveLiteral,
} from './ast';
import { createLookup } from './utilities';
import { ErrorNames, createMappedError } from './errors';
import { createImplementationRegister, DI } from '@aurelia/kernel';

export interface IExpressionParser<TCustom extends CustomExpression = CustomExpression> {
  parse(expression: string, expressionType: 'IsIterator'): ForOfStatement;
  parse(expression: string, expressionType: 'Interpolation'): Interpolation;
  parse(expression: string, expressionType: Exclude<ExpressionType, 'IsIterator' | 'Interpolation'>): IsBindingBehavior;
  parse(expression: string, expressionType: ExpressionType): AnyBindingExpression<TCustom>;
}
export const IExpressionParser = /*@__PURE__*/DI.createInterface<IExpressionParser>('IExpressionParser');

/**
 * A default implementation of the IExpressionParser interface
 */
export class ExpressionParser<TCustom extends CustomExpression = CustomExpression> implements IExpressionParser<TCustom> {
  public static readonly register = createImplementationRegister(IExpressionParser);

  /** @internal */ private readonly _expressionLookup: Record<string, IsBindingBehavior> = createLookup();
  /** @internal */ private readonly _forOfLookup: Record<string, ForOfStatement> = createLookup();
  /** @internal */ private readonly _interpolationLookup: Record<string, Interpolation> = createLookup();

  public parse(expression: string, expressionType: 'IsIterator'): ForOfStatement;
  public parse(expression: string, expressionType: 'Interpolation'): Interpolation;
  public parse(expression: string, expressionType: Exclude<ExpressionType, 'IsIterator' | 'Interpolation'>): IsBindingBehavior;
  public parse(expression: string, expressionType: ExpressionType): AnyBindingExpression;
  public parse(expression: string, expressionType: ExpressionType): AnyBindingExpression {
    let found: AnyBindingExpression;
    switch (expressionType) {
      case etIsCustom:
        return new CustomExpression(expression) as AnyBindingExpression;
      case etInterpolation:
        found = this._interpolationLookup[expression];
        if (found === void 0) {
          found = this._interpolationLookup[expression] = this.$parse(expression, expressionType);
        }
        return found;
      case etIsIterator:
        found = this._forOfLookup[expression];
        if (found === void 0) {
          found = this._forOfLookup[expression] = this.$parse(expression, expressionType);
        }
        return found;
      default: {
        if (expression.length === 0) {
          if (expressionType === etIsFunction || expressionType === etIsProperty) {
            return PrimitiveLiteral.$empty;
          }
          throw invalidEmptyExpression();
        }
        found = this._expressionLookup[expression];
        if (found === void 0) {
          found = this._expressionLookup[expression] = this.$parse(expression, expressionType);
        }
        return found;
      }
    }
  }

  /** @internal */
  private $parse(expression: string, expressionType: 'IsIterator'): ForOfStatement;
  /** @internal */
  private $parse(expression: string, expressionType: 'Interpolation'): Interpolation;
  /** @internal */
  private $parse(expression: string, expressionType: Exclude<ExpressionType, 'IsIterator' | 'Interpolation'>): IsBindingBehavior;
  /** @internal */
  private $parse(expression: string, expressionType: ExpressionType): AnyBindingExpression {
    $input = expression;
    $index = 0;
    $length = expression.length;
    $scopeDepth = 0;
    $startIndex = 0;
    $currentToken = Token.EOF;
    $tokenValue = '';
    $currentChar = $charCodeAt(0);
    $assignable = true;
    $optional = false;
    $accessGlobal = true;
    $semicolonIndex = -1;
    return parse(Precedence.Variadic, expressionType === void 0 ? etIsProperty : expressionType);
  }
}

_START_CONST_ENUM();
const enum Char {
  Null           = 0x00,
  Backspace      = 0x08,
  Tab            = 0x09,
  LineFeed       = 0x0A,
  VerticalTab    = 0x0B,
  FormFeed       = 0x0C,
  CarriageReturn = 0x0D,
  Space          = 0x20,
  Exclamation    = 0x21,
  DoubleQuote    = 0x22,
  Dollar         = 0x24,
  Percent        = 0x25,
  Ampersand      = 0x26,
  SingleQuote    = 0x27,
  OpenParen      = 0x28,
  CloseParen     = 0x29,
  Asterisk       = 0x2A,
  Plus           = 0x2B,
  Comma          = 0x2C,
  Minus          = 0x2D,
  Dot            = 0x2E,
  Slash          = 0x2F,
  Semicolon      = 0x3B,
  Backtick       = 0x60,
  OpenBracket    = 0x5B,
  Backslash      = 0x5C,
  CloseBracket   = 0x5D,
  Caret          = 0x5E,
  Underscore     = 0x5F,
  OpenBrace      = 0x7B,
  Bar            = 0x7C,
  CloseBrace     = 0x7D,
  Colon          = 0x3A,
  LessThan       = 0x3C,
  Equals         = 0x3D,
  GreaterThan    = 0x3E,
  Question       = 0x3F,

  Zero   = 0x30,
  One    = 0x31,
  Two    = 0x32,
  Three  = 0x33,
  Four   = 0x34,
  Five   = 0x35,
  Six    = 0x36,
  Seven  = 0x37,
  Eight  = 0x38,
  Nine   = 0x39,

  UpperA = 0x41,
  UpperB = 0x42,
  UpperC = 0x43,
  UpperD = 0x44,
  UpperE = 0x45,
  UpperF = 0x46,
  UpperG = 0x47,
  UpperH = 0x48,
  UpperI = 0x49,
  UpperJ = 0x4A,
  UpperK = 0x4B,
  UpperL = 0x4C,
  UpperM = 0x4D,
  UpperN = 0x4E,
  UpperO = 0x4F,
  UpperP = 0x50,
  UpperQ = 0x51,
  UpperR = 0x52,
  UpperS = 0x53,
  UpperT = 0x54,
  UpperU = 0x55,
  UpperV = 0x56,
  UpperW = 0x57,
  UpperX = 0x58,
  UpperY = 0x59,
  UpperZ = 0x5A,

  LowerA  = 0x61,
  LowerB  = 0x62,
  LowerC  = 0x63,
  LowerD  = 0x64,
  LowerE  = 0x65,
  LowerF  = 0x66,
  LowerG  = 0x67,
  LowerH  = 0x68,
  LowerI  = 0x69,
  LowerJ  = 0x6A,
  LowerK  = 0x6B,
  LowerL  = 0x6C,
  LowerM  = 0x6D,
  LowerN  = 0x6E,
  LowerO  = 0x6F,
  LowerP  = 0x70,
  LowerQ  = 0x71,
  LowerR  = 0x72,
  LowerS  = 0x73,
  LowerT  = 0x74,
  LowerU  = 0x75,
  LowerV  = 0x76,
  LowerW  = 0x77,
  LowerX  = 0x78,
  LowerY  = 0x79,
  LowerZ  = 0x7A
}
_END_CONST_ENUM();

function unescapeCode(code: number): number {
  switch (code) {
    case Char.LowerB: return Char.Backspace;
    case Char.LowerT: return Char.Tab;
    case Char.LowerN: return Char.LineFeed;
    case Char.LowerV: return Char.VerticalTab;
    case Char.LowerF: return Char.FormFeed;
    case Char.LowerR: return Char.CarriageReturn;
    case Char.DoubleQuote: return Char.DoubleQuote;
    case Char.SingleQuote: return Char.SingleQuote;
    case Char.Backslash: return Char.Backslash;
    default: return code;
  }
}

_START_CONST_ENUM();
const enum Precedence {
  Variadic                = 0b0000_111101,
  Assign                  = 0b0000_111110,
  Conditional             = 0b0000_111111,
  Assignment              = 0b0001_000000,
  NullishCoalescing       = 0b0010_000000,
  LogicalOR               = 0b0011_000000,
  LogicalAND              = 0b0100_000000,
  Equality                = 0b0101_000000,
  Relational              = 0b0110_000000,
  Additive                = 0b0111_000000,
  Multiplicative          = 0b1000_000000,
  Exponentiation          = 0b1001_000000,
  Binary                  = 0b1001_000001,
  Member                  = 0b1001_000010,
  LeftHandSide            = 0b1001_000011,
  Primary                 = 0b1001_000100,
  Unary                   = 0b1001_000101,
}
_END_CONST_ENUM();

_START_CONST_ENUM();
const enum Token {
  EOF                     = 0b1100000000000_0000_000000,
  ExpressionTerminal      = 0b1000000000000_0000_000000,
  AccessScopeTerminal     = 0b0100000000000_0000_000000,
  ClosingToken            = 0b0010000000000_0000_000000,
  OpeningToken            = 0b0001000000000_0000_000000,
  BinaryOp                = 0b0000100000000_0000_000000,
  UnaryOp                 = 0b0000010000000_0000_000000,
  LeftHandSide            = 0b0000001000000_0000_000000,
  StringOrNumericLiteral  = 0b0000000110000_0000_000000,
  NumericLiteral          = 0b0000000100000_0000_000000,
  StringLiteral           = 0b0000000010000_0000_000000,
  IdentifierName          = 0b0000000001100_0000_000000,
  // Keyword              = 0b0000000001000_0000_000000,
  Identifier              = 0b0000000000100_0000_000000,
  Contextual              = 0b0000000000010_0000_000000,
  OptionalSuffix          = 0b0000000001101_0000_000000,
  Precedence              = 0b0000000000000_1111_000000,
  Type                    = 0b0000000000000_0000_111111,
  FalseKeyword            = 0b0000000001000_0000_000000,
  TrueKeyword             = 0b0000000001000_0000_000001,
  NullKeyword             = 0b0000000001000_0000_000010,
  UndefinedKeyword        = 0b0000000001000_0000_000011,
  NewKeyword              = 0b0000000001000_0000_000100,
  ThisScope               = 0b0000000001100_0000_000101,
  AccessBoundary          = 0b0000000001100_0000_000110,
  // HostScope            = 0b0000000001100_0000_000111,
  ParentScope             = 0b0000000001100_0000_001000,
  OpenParen               = 0b0101001000001_0000_001001,
  OpenBrace               = 0b0001000000000_0000_001010,
  Dot                     = 0b0000001000000_0000_001011,
  DotDot                  = 0b0000000000000_0000_001100,
  DotDotDot               = 0b0000000000000_0000_001101,
  QuestionDot             = 0b0100001000000_0000_001110,
  CloseBrace              = 0b1110000000000_0000_001111,
  CloseParen              = 0b1110000000000_0000_010000,
  Comma                   = 0b1100000000000_0000_010011,
  OpenBracket             = 0b0101001000001_0000_010100,
  CloseBracket            = 0b1110000000000_0000_010101,
  Colon                   = 0b1100000000000_0000_010110,
  Semicolon               = 0b1100000000000_0000_010111,
  Question                = 0b1100000000000_0000_011000,
  Ampersand               = 0b1100000000000_0000_011001,
  Bar                     = 0b1100000000000_0000_011010,
  QuestionQuestion        = 0b1100100000000_0010_011011,
  BarBar                  = 0b1100100000000_0011_011100,
  AmpersandAmpersand      = 0b1100100000000_0100_011101,
  EqualsEquals            = 0b1100100000000_0101_011110,
  ExclamationEquals       = 0b1100100000000_0101_011111,
  EqualsEqualsEquals      = 0b1100100000000_0101_100000,
  ExclamationEqualsEquals = 0b1100100000000_0101_100001,
  LessThan                = 0b1100100000000_0110_100010,
  GreaterThan             = 0b1100100000000_0110_100011,
  LessThanEquals          = 0b1100100000000_0110_100100,
  GreaterThanEquals       = 0b1100100000000_0110_100101,
  InKeyword               = 0b1100100001000_0110_100110,
  InstanceOfKeyword       = 0b1100100001000_0110_100111,
  Plus                    = 0b0100110000000_0111_101000,
  Minus                   = 0b0100110000000_0111_101001,
  TypeofKeyword           = 0b0000010001000_0000_101010,
  VoidKeyword             = 0b0000010001000_0000_101011,
  Asterisk                = 0b1100100000000_1000_101100,
  Percent                 = 0b1100100000000_1000_101101,
  Slash                   = 0b1100100000000_1000_101110,
  AsteriskAsterisk        = 0b1100100000000_1001_101111,
  Equals                  = 0b1000000000000_0000_110000,
  Exclamation             = 0b0000010000000_0000_110001,
  TemplateTail            = 0b0100001000001_0000_110010,
  TemplateContinuation    = 0b0100001000001_0000_110011,
  OfKeyword               = 0b1000000001010_0000_110100,
  Arrow                   = 0b0000000000000_0000_110101,
  PlusEquals              = 0b1000000000000_0000_110110,
  MinusEquals             = 0b1000000000000_0000_110111,
  AsteriskEquals          = 0b1000000000000_0000_111000,
  SlashEquals             = 0b1000000000000_0000_111001,
  PlusPlus                = 0b0100010000000_0000_111010,
  MinusMinus              = 0b0100010000000_0000_111011,
}
_END_CONST_ENUM();

const $false = PrimitiveLiteral.$false;
const $true = PrimitiveLiteral.$true;
const $null = PrimitiveLiteral.$null;
const $undefined = PrimitiveLiteral.$undefined;
const $this = createAccessThisExpression(0);
const $parent = createAccessThisExpression(1);
const boundary = createAccessBoundaryExpression();

const etNone = 'None';
const etInterpolation = 'Interpolation';
const etIsIterator = 'IsIterator';
const etIsChainable = 'IsChainable';
const etIsFunction = 'IsFunction';
const etIsProperty = 'IsProperty';
const etIsCustom = 'IsCustom';
export type ExpressionType = 'None' | 'Interpolation' | 'IsIterator' | 'IsChainable' | 'IsFunction' | 'IsProperty' | 'IsCustom';

let $input: string = '';
let $index: number = 0;
let $length: number = 0;
let $scopeDepth: number = 0;
let $startIndex: number = 0;
let $currentToken: Token = Token.EOF;
let $tokenValue: string | number = '';
let $currentChar: number;
let $assignable: boolean = true;
let $optional: boolean = false;
let $accessGlobal: boolean = true;
let $semicolonIndex: number = -1;

const stringFromCharCode = String.fromCharCode;
const $charCodeAt = (index: number) => $input.charCodeAt(index);

const $tokenRaw = (): string => $input.slice($startIndex, $index);

const globalNames =
  ('Infinity NaN isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent' +
  ' Array BigInt Boolean Date Map Number Object RegExp Set String JSON Math Intl').split(' ');

export function parseExpression(input: string, expressionType?: ExpressionType): AnyBindingExpression {
  $input = input;
  $index = 0;
  $length = input.length;
  $scopeDepth = 0;
  $startIndex = 0;
  $currentToken = Token.EOF;
  $tokenValue = '';
  $currentChar = $charCodeAt(0);
  $assignable = true;
  $optional = false;
  $accessGlobal = true;
  $semicolonIndex = -1;
  return parse(Precedence.Variadic, expressionType === void 0 ? etIsProperty : expressionType);
}

// This is performance-critical code which follows a subset of the well-known ES spec.
// Knowing the spec, or parsers in general, will help with understanding this code and it is therefore not the
// single source of information for being able to figure it out.
// It generally does not need to change unless the spec changes or spec violations are found, or optimization
// opportunities are found (which would likely not fix these warnings in any case).
// It's therefore not considered to have any tangible impact on the maintainability of the code base.
// For reference, most of the parsing logic is based on: https://tc39.github.io/ecma262/#sec-ecmascript-language-expressions
// eslint-disable-next-line max-lines-per-function
export function parse(minPrecedence: Precedence, expressionType: ExpressionType): AnyBindingExpression {
  if (expressionType === etIsCustom) {
    return new CustomExpression($input);
  }

  if ($index === 0) {
    if (expressionType === etInterpolation) {
      return parseInterpolation();
    }
    nextToken();
    if ($currentToken & Token.ExpressionTerminal) {
      throw invalidStartOfExpression();
    }
  }

  $assignable = Precedence.Binary > minPrecedence;
  $optional = false;
  $accessGlobal = Precedence.LeftHandSide > minPrecedence;
  let optionalThisTail = false;
  let result = void 0 as unknown as IsExpressionOrStatement;
  let ancestor = 0;

  if ($currentToken & Token.UnaryOp) {
    /**
     * parseUnaryExpression
     *
     * https://tc39.github.io/ecma262/#sec-unary-operators
     *
     * UnaryExpression :
     * 1. LeftHandSideExpression
     * 2. void UnaryExpression
     * 3. typeof UnaryExpression
     * 4. + UnaryExpression
     * 5. - UnaryExpression
     * 6. ! UnaryExpression
     * 7. ++ UnaryExpression
     * 8. -- UnaryExpression
     *
     * IsValidAssignmentTarget
     * 2,3,4,5,6,7,8 = false
     * 1 = see parseLeftHandSideExpression
     *
     * Note: technically we should throw on +++ / ---, but there's nothing to gain from that
     */
    const op = TokenValues[$currentToken & Token.Type] as UnaryOperator;
    nextToken();
    result = createUnaryExpression(op, parse(Precedence.LeftHandSide, expressionType) as IsLeftHandSide);
    $assignable = false;
  } else {
    /**
     * parsePrimaryExpression
     *
     * https://tc39.github.io/ecma262/#sec-primary-expression
     *
     * PrimaryExpression :
     * 1. this
     * 2. IdentifierName
     * 3. Literal
     * 4. ArrayLiteralExpression
     * 5. ObjectLiteralExpression
     * 6. TemplateLiteral
     * 7. ParenthesizedExpression
     *
     * Literal :
     * NullLiteral
     * BooleanLiteral
     * NumericLiteral
     * StringLiteral
     *
     * ParenthesizedExpression :
     * ( AssignmentExpression )
     *
     * IsValidAssignmentTarget
     * 1,3,4,5,6,7 = false
     * 2 = true
     */
    primary: switch ($currentToken) {
      case Token.ParentScope: // $parent
        ancestor = $scopeDepth;
        $assignable = false;
        $accessGlobal = false;
        do {
          nextToken();
          ++ancestor;
          switch (($currentToken as Token)) {
            case Token.Dot:
              nextToken();
              if (($currentToken & Token.IdentifierName) === 0) {
                throw expectedIdentifier();
              }
              break;
            case Token.DotDot:
            case Token.DotDotDot:
              throw expectedIdentifier();
            case Token.QuestionDot:
              $optional = true;
              nextToken();
              if (($currentToken & Token.IdentifierName) === 0) {
                result = ancestor === 0 ? $this : ancestor === 1 ? $parent : createAccessThisExpression(ancestor);
                optionalThisTail = true;
                break primary;
              }
              break;
            default:
              if ($currentToken & Token.AccessScopeTerminal) {
                result = ancestor === 0 ? $this : ancestor === 1 ? $parent : createAccessThisExpression(ancestor);
                break primary;
              }
              throw invalidMemberExpression();
          }
        } while ($currentToken === Token.ParentScope);
        // falls through
      case Token.Identifier: { // identifier
        const id = $tokenValue as string;
        if (expressionType === etIsIterator) {
          result = createBindingIdentifier(id);
        } else if ($accessGlobal && globalNames.includes(id as (typeof globalNames)[number])) {
          result = createAccessGlobalExpression(id);
        } else if ($accessGlobal && id === 'import') {
          throw unexpectedImportKeyword();
        } else {
          result = createAccessScopeExpression(id, ancestor);
        }
        $assignable = !$optional;
        nextToken();
        if (consumeOpt(Token.Arrow)) {
          if (($currentToken as Token) === Token.OpenBrace) {
            throw functionBodyInArrowFn();
          }
          const _optional = $optional;
          const _scopeDepth = $scopeDepth;
          ++$scopeDepth;
          const body = parse(Precedence.Assign, etNone) as IsAssign;
          $optional = _optional;
          $scopeDepth = _scopeDepth;
          $assignable = false;
          result = createArrowFunction([createBindingIdentifier(id)], body);
        }
        break;
      }
      case Token.DotDot:
        throw unexpectedDoubleDot();
      case Token.DotDotDot:
        throw invalidSpreadOp();
      case Token.ThisScope: // $this
        $assignable = false;
        nextToken();
        switch ($scopeDepth) {
          case 0:
            result = $this;
            break;
          case 1:
            result = $parent;
            break;
          default:
            result = createAccessThisExpression($scopeDepth);
            break;
        }
        break;
      case Token.AccessBoundary: // this
        $assignable = false;
        nextToken();
        result = boundary;
        break;
      case Token.OpenParen:
        result = parseCoverParenthesizedExpressionAndArrowParameterList(expressionType);
        break;
      case Token.OpenBracket:
        result = $input.search(/\s+of\s+/) > $index ? parseArrayDestructuring() : parseArrayLiteralExpression(expressionType);
        break;
      case Token.OpenBrace:
        result = parseObjectLiteralExpression(expressionType);
        break;
      case Token.TemplateTail:
        result = createTemplateExpression([$tokenValue as string]);
        $assignable = false;
        nextToken();
        break;
      case Token.TemplateContinuation:
        result = parseTemplate(expressionType, result as IsLeftHandSide, false);
        break;
      case Token.StringLiteral:
      case Token.NumericLiteral:
        result = createPrimitiveLiteralExpression($tokenValue);
        $assignable = false;
        nextToken();
        break;
      case Token.NullKeyword:
      case Token.UndefinedKeyword:
      case Token.TrueKeyword:
      case Token.FalseKeyword:
        result = TokenValues[$currentToken & Token.Type] as PrimitiveLiteralExpression;
        $assignable = false;
        nextToken();
        break;
      case Token.NewKeyword: {
        nextToken();
        const callee = parse(Precedence.Member, expressionType) as IsLeftHandSide;
        let args: IsAssign[];
        if (($currentToken as Token) === Token.OpenParen) {
          args = parseArguments();
        } else {
          args = [];
          nextToken();
        }
        result = createNewExpression(callee, args);
        $assignable = false;
        break;
      }
      default:
        if ($index >= $length) {
          throw unexpectedEndOfExpression();
        } else {
          throw unconsumedToken();
        }
    }

    if (expressionType === etIsIterator) {
      return parseForOfStatement(result as BindingIdentifierOrPattern);
    }
    switch ($currentToken as Token) {
      case Token.PlusPlus:
      case Token.MinusMinus:
        result = createUnaryExpression(TokenValues[$currentToken & Token.Type] as UnaryOperator, result as IsLeftHandSide, 1);
        nextToken();
        $assignable = false;
        break;
    }
    if (Precedence.LeftHandSide < minPrecedence) {
      return result as any;
    }

    if (($currentToken as Token) === Token.DotDot || ($currentToken as Token) === Token.DotDotDot) {
      throw expectedIdentifier();
    }

    if (result.$kind === ekAccessThis) {
      switch ($currentToken as Token) {
        case Token.QuestionDot:
          $optional = true;
          $assignable = false;
          nextToken();
          if (($currentToken & Token.OptionalSuffix) === 0) {
            throw unexpectedTokenInOptionalChain();
          }

          if ($currentToken & Token.IdentifierName) {
            result = createAccessScopeExpression($tokenValue as string, result.ancestor);
            nextToken();
          } else if (($currentToken as Token) === Token.OpenParen) {
            result = createCallFunctionExpression(result as IsLeftHandSide, parseArguments(), true);
          } else if (($currentToken as Token) === Token.OpenBracket) {
            result = parseKeyedExpression(result, true);
          } else {
            throw invalidTaggedTemplateOnOptionalChain();
          }
          break;
        case Token.Dot:
          $assignable = !$optional;
          nextToken();
          if (($currentToken & Token.IdentifierName) === 0) {
            throw expectedIdentifier();
          }
          result = createAccessScopeExpression($tokenValue as string, result.ancestor);
          nextToken();
          break;
        case Token.DotDot:
        case Token.DotDotDot:
          throw expectedIdentifier();
        case Token.OpenParen:
          result = createCallFunctionExpression(result as IsLeftHandSide, parseArguments(), optionalThisTail);
          break;
        case Token.OpenBracket:
          result = parseKeyedExpression(result, optionalThisTail);
          break;
        case Token.TemplateTail:
          result = createTemplateTail(result as IsLeftHandSide);
          break;
        case Token.TemplateContinuation:
          result = parseTemplate(expressionType, result as IsLeftHandSide, true);
          break;
      }
    }

    /**
     * parseMemberExpression (Token.Dot, Token.OpenBracket, Token.TemplateContinuation)
     *
     * MemberExpression :
     * 1. PrimaryExpression
     * 2. MemberExpression [ AssignmentExpression ]
     * 3. MemberExpression . IdentifierName
     * 4. MemberExpression TemplateLiteral
     *
     * IsValidAssignmentTarget
     * 1,4 = false
     * 2,3 = true
     *
     *
     * parseCallExpression (Token.OpenParen)
     * CallExpression :
     * 1. MemberExpression Arguments
     * 2. CallExpression Arguments
     * 3. CallExpression [ AssignmentExpression ]
     * 4. CallExpression . IdentifierName
     * 5. CallExpression TemplateLiteral
     *
     * IsValidAssignmentTarget
     * 1,2,5 = false
     * 3,4 = true
     */
    while (($currentToken & Token.LeftHandSide) > 0) {
      switch (($currentToken as Token)) {
        case Token.QuestionDot:
          result = parseOptionalChainLHS(result as IsLeftHandSide);
          break;
        case Token.Dot:
          nextToken();
          if (($currentToken & Token.IdentifierName) === 0) {
            throw expectedIdentifier();
          }
          result = parseMemberExpressionLHS(result as IsLeftHandSide, false);
          break;
        case Token.DotDot:
        case Token.DotDotDot:
          throw expectedIdentifier();
        case Token.OpenParen:
          if (Precedence.Member === minPrecedence) {
            return result as any;
          }
          if (result.$kind === ekAccessScope) {
            result = createCallScopeExpression(result.name, parseArguments(), result.ancestor, false);
          } else if (result.$kind === ekAccessMember) {
            result = createCallMemberExpression(result.object, result.name, parseArguments(), result.optional, false);
          } else if (result.$kind === ekAccessGlobal) {
            result = createCallGlobalExpression(result.name, parseArguments());
          } else {
            result = createCallFunctionExpression(result as IsLeftHandSide, parseArguments(), false);
          }
          break;
        case Token.OpenBracket:
          result = parseKeyedExpression(result as IsLeftHandSide, false);
          break;
        case Token.TemplateTail:
          if ($optional) {
            throw invalidTaggedTemplateOnOptionalChain();
          }
          result = createTemplateTail(result as IsLeftHandSide);
          break;
        case Token.TemplateContinuation:
          if ($optional) {
            throw invalidTaggedTemplateOnOptionalChain();
          }
          result = parseTemplate(expressionType, result as IsLeftHandSide, true);
          break;
      }
    }
  }

  if (($currentToken as Token) === Token.DotDot || ($currentToken as Token) === Token.DotDotDot) {
    throw expectedIdentifier();
  }

  if (Precedence.Binary < minPrecedence) {
    return result as any;
  }

  /**
   * parseBinaryExpression
   *
   * https://tc39.github.io/ecma262/#sec-multiplicative-operators
   *
   * MultiplicativeExpression : (local precedence 6)
   * UnaryExpression
   * MultiplicativeExpression * / % UnaryExpression
   *
   * AdditiveExpression : (local precedence 5)
   * MultiplicativeExpression
   * AdditiveExpression + - MultiplicativeExpression
   *
   * RelationalExpression : (local precedence 4)
   * AdditiveExpression
   * RelationalExpression < > <= >= instanceof in AdditiveExpression
   *
   * EqualityExpression : (local precedence 3)
   * RelationalExpression
   * EqualityExpression == != === !== RelationalExpression
   *
   * LogicalANDExpression : (local precedence 2)
   * EqualityExpression
   * LogicalANDExpression && EqualityExpression
   *
   * LogicalORExpression : (local precedence 1)
   * LogicalANDExpression
   * LogicalORExpression || LogicalANDExpression
   *
   * CoalesceExpression :
   * CoalesceExpressionHead ?? BitwiseORExpression
   *
   * CoalesceExpressionHead :
   * CoelesceExpression
   * BitwiseORExpression
   *
   * ShortCircuitExpression :
   * LogicalORExpression
   * CoalesceExpression
   */
  while (($currentToken & Token.BinaryOp) > 0) {
    const opToken = $currentToken;
    if ((opToken & Token.Precedence) <= minPrecedence) {
      break;
    }
    nextToken();
    result = createBinaryExpression(TokenValues[opToken & Token.Type] as BinaryOperator, result as IsBinary, parse(opToken & Token.Precedence, expressionType) as IsBinary);
    $assignable = false;
  }
  if (Precedence.Conditional < minPrecedence) {
    return result as any;
  }

  /**
   * parseConditionalExpression
   * https://tc39.github.io/ecma262/#prod-ConditionalExpression
   *
   * ConditionalExpression :
   * 1. ShortCircuitExpression
   * 2. ShortCircuitExpression ? AssignmentExpression : AssignmentExpression
   *
   * IsValidAssignmentTarget
   * 1,2 = false
   */

  if (consumeOpt(Token.Question)) {
    const yes = parse(Precedence.Assign, expressionType) as IsAssign;
    consume(Token.Colon);
    result = createConditionalExpression(result as IsBinary, yes, parse(Precedence.Assign, expressionType) as IsAssign);
    $assignable = false;
  }
  if (Precedence.Assign < minPrecedence) {
    return result as any;
  }

  /**
   * parseAssignmentExpression
   *
   * https://tc39.github.io/ecma262/#prod-AssignmentExpression
   * Note: AssignmentExpression here is equivalent to ES Expression because we don't parse the comma operator
   *
   * AssignmentExpression :
   * 1. ConditionalExpression
   * 2. LeftHandSideExpression = AssignmentExpression
   * 3. LeftHandSideExpression AssignmentOperator AssignmentExpression
   *
   * IsValidAssignmentTarget
   * 1,2 = false
   */

  switch ($currentToken as Token) {
    case Token.Equals:
    case Token.PlusEquals:
    case Token.MinusEquals:
    case Token.AsteriskEquals:
    case Token.SlashEquals: {
      if (!$assignable) {
        throw lhsNotAssignable();
      }
      const op = TokenValues[$currentToken & Token.Type] as AssignmentOperator;
      nextToken();
      result = createAssignExpression(result as IsAssignable, parse(Precedence.Assign, expressionType) as IsAssign, op);
      break;
    }
  }
  if (Precedence.Variadic < minPrecedence) {
    return result as any;
  }

  /**
   * parseValueConverter
   */
  while (consumeOpt(Token.Bar)) {
    if ($currentToken === Token.EOF) {
      throw expectedValueConverterIdentifier();
    }
    const name = $tokenValue as string;
    nextToken();
    const args = new Array<IsAssign>();
    while (consumeOpt(Token.Colon)) {
      args.push(parse(Precedence.Assign, expressionType) as IsAssign);
    }
    result = createValueConverterExpression(result as IsValueConverter, name, args);
  }

  /**
   * parseBindingBehavior
   */
  while (consumeOpt(Token.Ampersand)) {
    if ($currentToken === Token.EOF) {
      throw expectedBindingBehaviorIdentifier();
    }
    const name = $tokenValue as string;
    nextToken();
    const args = new Array<IsAssign>();
    while (consumeOpt(Token.Colon)) {
      args.push(parse(Precedence.Assign, expressionType) as IsAssign);
    }
    result = createBindingBehaviorExpression(result as IsBindingBehavior, name, args);
  }

  if ($currentToken !== Token.EOF) {
    if (expressionType === etInterpolation && $currentToken === Token.CloseBrace) {
      return result as any;
    }
    if (expressionType === etIsChainable && $currentToken === Token.Semicolon) {
      if ($index === $length) {
        throw unconsumedToken();
      }
      $semicolonIndex = $index - 1;
      return result as any;
    }
    if ($tokenRaw() === 'of') {
      throw unexpectedOfKeyword();
    }
    throw unconsumedToken();
  }
  return result as any;
}

/**
 * [key,]
 * [key]
 * [,value]
 * [key,value]
 */
function parseArrayDestructuring(): DAE {
  const items: DAE['list'][number][] = [];
  let target: string | null = null;
  let $continue = true;
  let index = 0;
  while ($continue) {
    nextToken();
    switch ($currentToken) {
      case Token.CloseBracket:
        $continue = false;
        addItem();
        break;
      case Token.Comma:
        addItem();
        break;
      case Token.Identifier:
        target = $tokenRaw();
        break;
      default:
        throw unexpectedTokenInDestructuring();
    }
  }
  consume(Token.CloseBracket);
  return createDestructuringAssignmentExpression(ekArrayDestructuring, items, void 0, void 0);

  function addItem() {
    if (target !== null) {
      items.push(
        createDestructuringAssignmentSingleExpression(
          createAccessMemberExpression($this, target),
          createAccessKeyedExpression($this, createPrimitiveLiteralExpression(index++)),
          void 0,
        ),
      );
      target = null;
    } else {
      index++;
    }
  }
}

function parseArguments() {
  const _optional = $optional;

  nextToken();
  const args: IsAssign[] = [];
  while (($currentToken as Token) !== Token.CloseParen) {
    args.push(parse(Precedence.Assign, etNone) as IsAssign);
    if (!consumeOpt(Token.Comma)) {
      break;
    }
  }
  consume(Token.CloseParen);

  $assignable = false;
  $optional = _optional;

  return args;
}

function parseKeyedExpression(result: IsLeftHandSide, optional: boolean) {
  const _optional = $optional;

  nextToken();
  result = createAccessKeyedExpression(result, parse(Precedence.Assign, etNone) as IsAssign, optional);
  consume(Token.CloseBracket);

  $assignable = !_optional;
  $optional = _optional;

  return result;
}

function parseOptionalChainLHS(lhs: IsLeftHandSide) {
  $optional = true;
  $assignable = false;
  nextToken();
  if (($currentToken & Token.OptionalSuffix) === 0) {
    throw unexpectedTokenInOptionalChain();
  }

  if ($currentToken & Token.IdentifierName) {
    return parseMemberExpressionLHS(lhs, true);
  }

  if (($currentToken as Token) === Token.OpenParen) {
    if (lhs.$kind === ekAccessScope) {
      return createCallScopeExpression(lhs.name, parseArguments(), lhs.ancestor, true);
    } else if (lhs.$kind === ekAccessMember) {
      return createCallMemberExpression(lhs.object, lhs.name, parseArguments(), lhs.optional, true);
    } else {
      return createCallFunctionExpression(lhs, parseArguments(), true);
    }
  }

  if (($currentToken as Token) === Token.OpenBracket) {
    return parseKeyedExpression(lhs, true);
  }

  throw invalidTaggedTemplateOnOptionalChain();
}

function parseMemberExpressionLHS(lhs: IsLeftHandSide, optional: boolean) {
  const rhs = $tokenValue as string;
  switch (($currentToken as Token)) {
    case Token.QuestionDot: {
      $optional = true;
      $assignable = false;

      const indexSave = $index;
      const startIndexSave = $startIndex;
      const currentTokenSave = $currentToken;
      const currentCharSave = $currentChar;
      const tokenValueSave = $tokenValue;
      const assignableSave = $assignable;
      const optionalSave = $optional;

      nextToken();
      if (($currentToken & Token.OptionalSuffix) === 0) {
        throw unexpectedTokenInOptionalChain();
      }

      if (($currentToken as Token) === Token.OpenParen) {
        return createCallMemberExpression(lhs, rhs, parseArguments(), optional, true);
      }

      $index = indexSave;
      $startIndex = startIndexSave;
      $currentToken = currentTokenSave;
      $currentChar = currentCharSave;
      $tokenValue = tokenValueSave;
      $assignable = assignableSave;
      $optional = optionalSave;

      return createAccessMemberExpression(lhs, rhs, optional);
    }
    case Token.OpenParen: {
      $assignable = false;
      return createCallMemberExpression(lhs, rhs, parseArguments(), optional, false);
    }
    default: {
      $assignable = !$optional;
      nextToken();
      return createAccessMemberExpression(lhs, rhs, optional);
    }
  }
}

_START_CONST_ENUM();
const enum ArrowFnParams {
  Valid         = 1,
  Invalid       = 2,
  Default       = 3,
  Destructuring = 4,
}
_END_CONST_ENUM();

/**
 * https://tc39.es/ecma262/#prod-CoverParenthesizedExpressionAndArrowParameterList
 * CoverParenthesizedExpressionAndArrowParameterList :
 * ( Expression )
 * ( )
 * ( BindingIdentifier )
 * ( Expression , BindingIdentifier )
 */
function parseCoverParenthesizedExpressionAndArrowParameterList(expressionType: ExpressionType): IsAssign {
  nextToken();

  const indexSave = $index;
  const startIndexSave = $startIndex;
  const currentTokenSave = $currentToken;
  const currentCharSave = $currentChar;
  const tokenValueSave = $tokenValue;
  const optionalSave = $optional;

  const arrowParams: BindingIdentifier[] = [];
  let paramsState = ArrowFnParams.Valid;
  let isParamList = false;

  // eslint-disable-next-line no-constant-condition
  loop: while (true) {
    if (($currentToken as Token) === Token.DotDotDot) {
      nextToken();
      if (($currentToken as Token) !== Token.Identifier) {
        throw expectedIdentifier();
      }
      arrowParams.push(createBindingIdentifier($tokenValue as string));

      nextToken();
      if (($currentToken as Token) === Token.Comma) {
        throw restParamsMustBeLastParam();
      }

      if (($currentToken as Token) !== Token.CloseParen) {
        throw invalidSpreadOp();
      }

      nextToken();
      if (($currentToken as Token) !== Token.Arrow) {
        throw invalidSpreadOp();
      }

      nextToken();
      const _optional = $optional;
      const _scopeDepth = $scopeDepth;
      ++$scopeDepth;
      const body = parse(Precedence.Assign, etNone) as IsAssign;
      $optional = _optional;
      $scopeDepth = _scopeDepth;
      $assignable = false;
      return createArrowFunction(arrowParams, body, true);
    }

    switch ($currentToken as Token) {
      case Token.Identifier:
        arrowParams.push(createBindingIdentifier($tokenValue as string));
        nextToken();
        break;
      case Token.CloseParen:
        // ()     - only valid if followed directly by an arrow
        nextToken();
        break loop;
      /* eslint-disable */
      case Token.OpenBrace:
        // ({     - may be a valid parenthesized expression
      case Token.OpenBracket:
        // ([     - may be a valid parenthesized expression
        nextToken();
        paramsState = ArrowFnParams.Destructuring;
        break;
      /* eslint-enable */
      case Token.Comma:
        // (,     - never valid
        // (a,,   - never valid
        paramsState = ArrowFnParams.Invalid;
        isParamList = true;
        break loop;
      case Token.OpenParen:
        // ((     - may be a valid nested parenthesized expression or arrow fn
        // (a,(   - never valid
        paramsState = ArrowFnParams.Invalid;
        break loop;
      default:
        nextToken();
        paramsState = ArrowFnParams.Invalid;
        break;
    }

    switch ($currentToken) {
      case Token.Comma:
        nextToken();
        isParamList = true;
        if (paramsState === ArrowFnParams.Valid) {
          break;
        }
        // ([something invalid],   - treat as arrow fn / invalid arrow params
        break loop;
      case Token.CloseParen:
        nextToken();
        break loop;
      case Token.Equals:
        // (a=a     - may be a valid parenthesized expression
        if (paramsState === ArrowFnParams.Valid) {
          paramsState = ArrowFnParams.Default;
        }
        break loop;
      case Token.Arrow:
        // (a,a=>  - never valid
        if (isParamList) {
          throw invalidArrowParameterList();
        }
        // (a=>    - may be a valid parenthesized expression with nested arrow fn
        nextToken();
        paramsState = ArrowFnParams.Invalid;
        break loop;
      default:
        if (paramsState === ArrowFnParams.Valid) {
          paramsState = ArrowFnParams.Invalid;
        }
        break loop;
    }
  }

  if ($currentToken === Token.Arrow) {
    if (paramsState === ArrowFnParams.Valid) {
      nextToken();
      if (($currentToken as Token) === Token.OpenBrace) {
        throw functionBodyInArrowFn();
      }
      const _optional = $optional;
      const _scopeDepth = $scopeDepth;
      ++$scopeDepth;
      const body = parse(Precedence.Assign, etNone) as IsAssign;
      $optional = _optional;
      $scopeDepth = _scopeDepth;
      $assignable = false;
      return createArrowFunction(arrowParams, body);
    }
    throw invalidArrowParameterList();
  } else if (paramsState === ArrowFnParams.Valid && arrowParams.length === 0) {
    // ()    - never valid as a standalone expression
    throw missingExpectedToken(Token.Arrow);
  }

  if (isParamList) {
    // ([something invalid],   - treat as arrow fn / invalid arrow params
    switch (paramsState) {
      case ArrowFnParams.Invalid:
        throw invalidArrowParameterList();
      case ArrowFnParams.Default:
        throw defaultParamsInArrowFn();
      case ArrowFnParams.Destructuring:
        throw destructuringParamsInArrowFn();
    }
  }

  $index = indexSave;
  $startIndex = startIndexSave;
  $currentToken = currentTokenSave;
  $currentChar = currentCharSave;
  $tokenValue = tokenValueSave;
  $optional = optionalSave;

  const _optional = $optional;
  const expr = parse(Precedence.Assign, expressionType) as IsAssign;
  $optional = _optional;
  consume(Token.CloseParen);

  if ($currentToken === Token.Arrow) {
    // we only get here if there was a valid parenthesized expression which was not valid as arrow fn params
    switch (paramsState) {
      case ArrowFnParams.Invalid:
        throw invalidArrowParameterList();
      case ArrowFnParams.Default:
        throw defaultParamsInArrowFn();
      case ArrowFnParams.Destructuring:
        throw destructuringParamsInArrowFn();
    }
  }

  return expr;
}

/**
 * parseArrayLiteralExpression
 * https://tc39.github.io/ecma262/#prod-ArrayLiteralExpression
 *
 * ArrayLiteralExpression :
 * [ Elision(opt) ]
 * [ ElementList ]
 * [ ElementList, Elision(opt) ]
 *
 * ElementList :
 * Elision(opt) AssignmentExpression
 * ElementList, Elision(opt) AssignmentExpression
 *
 * Elision :
 * ,
 * Elision ,
 */
function parseArrayLiteralExpression(expressionType: ExpressionType): ArrayBindingPattern | ArrayLiteralExpression {
  const _optional = $optional;

  nextToken();
  const elements = new Array<IsAssign>();
  while ($currentToken !== Token.CloseBracket) {
    if (consumeOpt(Token.Comma)) {
      elements.push($undefined);
      if (($currentToken as Token) === Token.CloseBracket) {
        break;
      }
    } else {
      elements.push(parse(Precedence.Assign, expressionType === etIsIterator ? etNone : expressionType) as IsAssign);
      if (consumeOpt(Token.Comma)) {
        if (($currentToken as Token) === Token.CloseBracket) {
          break;
        }
      } else {
        break;
      }
    }
  }

  $optional = _optional;

  consume(Token.CloseBracket);
  if (expressionType === etIsIterator) {
    return createArrayBindingPattern(elements);
  } else {
    $assignable = false;
    return createArrayLiteralExpression(elements);
  }
}

const allowedForExprKinds: ExpressionKind[] = [ekArrayBindingPattern, ekObjectBindingPattern, ekBindingIdentifier, ekArrayDestructuring, ekObjectDestructuring];
function parseForOfStatement(result: BindingIdentifierOrPattern): ForOfStatement {
  if (!allowedForExprKinds.includes(result.$kind)) {
    throw invalidLHSBindingIdentifierInForOf(result.$kind);
  }
  if ($currentToken !== Token.OfKeyword) {
    throw invalidLHSBindingIdentifierInForOf(result.$kind);
  }
  nextToken();
  const declaration = result;
  const statement = parse(Precedence.Variadic, etIsChainable);
  return createForOfStatement(declaration, statement as IsBindingBehavior, $semicolonIndex);
}

/**
 * parseObjectLiteralExpression
 * https://tc39.github.io/ecma262/#prod-Literal
 *
 * ObjectLiteralExpression :
 * { }
 * { PropertyDefinitionList }
 *
 * PropertyDefinitionList :
 * PropertyDefinition
 * PropertyDefinitionList, PropertyDefinition
 *
 * PropertyDefinition :
 * IdentifierName
 * PropertyName : AssignmentExpression
 *
 * PropertyName :
 * IdentifierName
 * StringLiteral
 * NumericLiteral
 */
function parseObjectLiteralExpression(expressionType: ExpressionType): ObjectBindingPattern | ObjectLiteralExpression {
  const _optional = $optional;

  const keys = new Array<string | number>();
  const values = new Array<IsAssign>();
  nextToken();
  while ($currentToken !== Token.CloseBrace) {
    keys.push($tokenValue);
    // Literal = mandatory colon
    if ($currentToken & Token.StringOrNumericLiteral) {
      nextToken();
      consume(Token.Colon);
      values.push(parse(Precedence.Assign, expressionType === etIsIterator ? etNone : expressionType) as IsAssign);
    } else if ($currentToken & Token.IdentifierName) {
      // IdentifierName = optional colon
      const currentChar = $currentChar;
      const currentToken = $currentToken;
      const index = $index;
      nextToken();
      if (consumeOpt(Token.Colon)) {
        values.push(parse(Precedence.Assign, expressionType === etIsIterator ? etNone : expressionType) as IsAssign);
      } else {
        // Shorthand
        $currentChar = currentChar;
        $currentToken = currentToken;
        $index = index;
        values.push(parse(Precedence.Primary, expressionType === etIsIterator ? etNone : expressionType) as IsAssign);
      }
    } else {
      throw invalidPropDefInObjLiteral();
    }
    if (($currentToken as Token) !== Token.CloseBrace) {
      consume(Token.Comma);
    }
  }

  $optional = _optional;

  consume(Token.CloseBrace);
  if (expressionType === etIsIterator) {
    return createObjectBindingPattern(keys, values);
  } else {
    $assignable = false;
    return createObjectLiteralExpression(keys, values);
  }
}

function parseInterpolation(): Interpolation {
  const parts = [];
  const expressions: (IsBindingBehavior | Interpolation)[] = [];
  const length = $length;
  let result = '';
  while ($index < length) {
    switch ($currentChar) {
      case Char.Dollar:
        if ($charCodeAt($index + 1) === Char.OpenBrace) {
          parts.push(result);
          result = '';

          $index += 2;
          $currentChar = $charCodeAt($index);
          nextToken();
          const expression = parse(Precedence.Variadic, etInterpolation) as IsBindingBehavior | Interpolation;
          expressions.push(expression);
          continue;
        } else {
          result += '$';
        }
        break;
      case Char.Backslash:
        result += stringFromCharCode(unescapeCode(nextChar()));
        break;
      default:
        result += stringFromCharCode($currentChar);
    }
    nextChar();
  }
  if (expressions.length) {
    parts.push(result);
    return createInterpolationAst(parts, expressions as IsBindingBehavior[]);
  }
  return null!;
}

/**
 * parseTemplateLiteralExpression
 * https://tc39.github.io/ecma262/#prod-Literal
 *
 * TemplateExpression :
 * NoSubstitutionTemplate
 * TemplateHead
 *
 * NoSubstitutionTemplate :
 * ` TemplateCharacters(opt) `
 *
 * TemplateHead :
 * ` TemplateCharacters(opt) ${
 *
 * TemplateSubstitutionTail :
 * TemplateMiddle
 * TemplateTail
 *
 * TemplateMiddle :
 * } TemplateCharacters(opt) ${
 *
 * TemplateTail :
 * } TemplateCharacters(opt) `
 *
 * TemplateCharacters :
 * TemplateCharacter TemplateCharacters(opt)
 *
 * TemplateCharacter :
 * $ [lookahead ≠ {]
 * \ EscapeSequence
 * SourceCharacter (but not one of ` or \ or $)
 */
function parseTemplate(expressionType: ExpressionType, result: IsLeftHandSide, tagged: boolean): TaggedTemplateExpression | TemplateExpression {
  const _optional = $optional;

  const cooked = [$tokenValue as string];
  // TODO: properly implement raw parts / decide whether we want this
  consume(Token.TemplateContinuation);
  const expressions = [parse(Precedence.Assign, expressionType) as IsAssign];
  while (($currentToken = scanTemplateTail()) !== Token.TemplateTail) {
    cooked.push($tokenValue as string);
    consume(Token.TemplateContinuation);
    expressions.push(parse(Precedence.Assign, expressionType) as IsAssign);
  }
  cooked.push($tokenValue as string);

  $assignable = false;
  $optional = _optional;
  if (tagged) {
    nextToken();
    return createTaggedTemplateExpression(cooked, cooked, result, expressions);
  } else {
    nextToken();
    return createTemplateExpression(cooked, expressions);
  }
}

function createTemplateTail(result: IsLeftHandSide) {
  $assignable = false;
  const strings = [$tokenValue as string];
  nextToken();
  return createTaggedTemplateExpression(strings, strings, result);
}

function nextToken(): void {
  while ($index < $length) {
    $startIndex = $index;
    if (($currentToken = (CharScanners[$currentChar]()) as Token) != null) { // a null token means the character must be skipped
      return;
    }
  }
  $currentToken = Token.EOF;
}

function nextChar(): number {
  return $currentChar = $charCodeAt(++$index);
}

function scanIdentifier(): Token {
  // run to the next non-idPart
  while (IdParts[nextChar()]);

  const token: Token|undefined = KeywordLookup[$tokenValue = $tokenRaw()];
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
  return token === undefined ? Token.Identifier : token;
}

function scanNumber(isFloat: boolean): Token {
  let char = $currentChar;
  if (isFloat === false) {
    do {
      char = nextChar();
    } while (char <= Char.Nine && char >= Char.Zero);

    if (char !== Char.Dot) {
      $tokenValue = parseInt($tokenRaw(), 10);
      return Token.NumericLiteral;
    }
    // past this point it's always a float
    char = nextChar();
    if ($index >= $length) {
      // unless the number ends with a dot - that behaves a little different in native ES expressions
      // but in our AST that behavior has no effect because numbers are always stored in variables
      $tokenValue = parseInt($tokenRaw().slice(0, -1), 10);
      return Token.NumericLiteral;
    }
  }

  if (char <= Char.Nine && char >= Char.Zero) {
    do {
      char = nextChar();
    } while (char <= Char.Nine && char >= Char.Zero);
  } else {
    $currentChar = $charCodeAt(--$index);
  }

  $tokenValue = parseFloat($tokenRaw());
  return Token.NumericLiteral;
}

function scanString(): Token {
  const quote = $currentChar;
  nextChar(); // Skip initial quote.

  let unescaped = 0;
  const buffer = new Array<string>();
  let marker = $index;

  while ($currentChar !== quote) {
    if ($currentChar === Char.Backslash) {
      buffer.push($input.slice(marker, $index));
      nextChar();
      unescaped = unescapeCode($currentChar);
      nextChar();
      buffer.push(stringFromCharCode(unescaped));
      marker = $index;
    } else if ($index >= $length) {
      throw unterminatedStringLiteral();
    } else {
      nextChar();
    }
  }

  const last = $input.slice(marker, $index);
  nextChar(); // Skip terminating quote.

  // Compute the unescaped string value.
  buffer.push(last);
  const unescapedStr = buffer.join('');

  $tokenValue = unescapedStr;
  return Token.StringLiteral;
}

function scanTemplate(): Token {
  let tail = true;
  let result = '';

  while (nextChar() !== Char.Backtick) {
    if ($currentChar === Char.Dollar) {
      if (($index + 1) < $length && $charCodeAt($index + 1) === Char.OpenBrace) {
        $index++;
        tail = false;
        break;
      } else {
        result += '$';
      }
    } else if ($currentChar === Char.Backslash) {
      result += stringFromCharCode(unescapeCode(nextChar()));
    } else {
      if ($index >= $length) {
        throw unterminatedTemplateLiteral();
      }
      result += stringFromCharCode($currentChar);
    }
  }

  nextChar();
  $tokenValue = result;
  if (tail) {
    return Token.TemplateTail;
  }
  return Token.TemplateContinuation;
}

const scanTemplateTail = (): Token => {
  if ($index >= $length) {
    throw unterminatedTemplateLiteral();
  }
  $index--;
  return scanTemplate();
};

const consumeOpt = (token: Token): boolean => {
  if ($currentToken === token) {
    nextToken();
    return true;
  }

  return false;
};

const consume = (token: Token): void => {
  if ($currentToken === token) {
    nextToken();
  } else {
    throw missingExpectedToken(token);
  }
};

// #region errors

const invalidStartOfExpression = () => createMappedError(ErrorNames.parse_invalid_start, $input);

const invalidSpreadOp = () => createMappedError(ErrorNames.parse_no_spread, $input);

const expectedIdentifier = () => createMappedError(ErrorNames.parse_expected_identifier, $input);

const invalidMemberExpression = () => createMappedError(ErrorNames.parse_invalid_member_expr, $input);

const unexpectedEndOfExpression = () => createMappedError(ErrorNames.parse_unexpected_end, $input);

const unconsumedToken = () => createMappedError(ErrorNames.parse_unconsumed_token, $tokenRaw(), $index, $input);

const invalidEmptyExpression = () => createMappedError(ErrorNames.parse_invalid_empty);

const lhsNotAssignable = () => createMappedError(ErrorNames.parse_left_hand_side_not_assignable, $input);

const expectedValueConverterIdentifier = () => createMappedError(ErrorNames.parse_expected_converter_identifier, $input);

const expectedBindingBehaviorIdentifier = () => createMappedError(ErrorNames.parse_expected_behavior_identifier, $input);

const unexpectedOfKeyword = () => createMappedError(ErrorNames.parse_unexpected_keyword_of, $input);

const unexpectedImportKeyword = () => createMappedError(ErrorNames.parse_unexpected_keyword_import, $input);

const invalidLHSBindingIdentifierInForOf = (kind: unknown) => createMappedError(ErrorNames.parse_invalid_identifier_in_forof, $input, kind);

const invalidPropDefInObjLiteral = () => createMappedError(ErrorNames.parse_invalid_identifier_object_literal_key, $input);

const unterminatedStringLiteral = () => createMappedError(ErrorNames.parse_unterminated_string, $input);

const unterminatedTemplateLiteral = () => createMappedError(ErrorNames.parse_unterminated_template_string, $input);

const missingExpectedToken = (token: Token) =>
  __DEV__
    ? createMappedError(ErrorNames.parse_missing_expected_token, TokenValues[token & Token.Type], $input)
    : createMappedError(ErrorNames.parse_missing_expected_token, $input);

const unexpectedTokenInDestructuring = () =>
  __DEV__
    ? createMappedError(ErrorNames.parse_unexpected_token_destructuring, $tokenRaw(), $index, $input)
    : createMappedError(ErrorNames.parse_unexpected_token_destructuring, $input);

const unexpectedTokenInOptionalChain = () =>
  __DEV__
    ? createMappedError(ErrorNames.parse_unexpected_token_optional_chain, $tokenRaw(), $index - 1, $input)
    : createMappedError(ErrorNames.parse_unexpected_token_optional_chain, $input);

const invalidTaggedTemplateOnOptionalChain = () => createMappedError(ErrorNames.parse_invalid_tag_in_optional_chain, $input);

const invalidArrowParameterList = () => createMappedError(ErrorNames.parse_invalid_arrow_params, $input);

const defaultParamsInArrowFn = () => createMappedError(ErrorNames.parse_no_arrow_param_default_value, $input);

const destructuringParamsInArrowFn = () => createMappedError(ErrorNames.parse_no_arrow_param_destructuring, $input);

const restParamsMustBeLastParam = () => createMappedError(ErrorNames.parse_rest_must_be_last, $input);

const functionBodyInArrowFn = () => createMappedError(ErrorNames.parse_no_arrow_fn_body, $input);

const unexpectedDoubleDot = () =>
  __DEV__
    ? createMappedError(ErrorNames.parse_unexpected_double_dot, $index - 1, $input)
    : createMappedError(ErrorNames.parse_unexpected_double_dot, $input);

// #endregion

/**
 * Array for mapping tokens to token values. The indices of the values
 * correspond to the token bits 0-38.
 * For this to work properly, the values in the array must be kept in
 * the same order as the token bits.
 * Usage: TokenValues[token & Token.Type]
 */
const TokenValues = [
  $false, $true, $null, $undefined, 'new', 'this', '$this', null/* '$host' */, '$parent',

  '(', '{', '.', '..', '...', '?.', '}', ')', ',', '[', ']', ':', ';', '?', '\'', '"',

  '&', '|', '??', '||', '&&', '==', '!=', '===', '!==', '<', '>',
  '<=', '>=', 'in', 'instanceof', '+', '-', 'typeof', 'void', '*', '%', '/', '**', '=', '!',
  Token.TemplateTail, Token.TemplateContinuation,
  'of', '=>', '+=', '-=', '*=', '/=', '++', '--'
];

const KeywordLookup: Record<string, Token> = /*@__PURE__*/ Object.assign(createLookup<Token>(), {
  true: Token.TrueKeyword,
  null: Token.NullKeyword,
  false: Token.FalseKeyword,
  undefined: Token.UndefinedKeyword,
  new: Token.NewKeyword,
  this: Token.AccessBoundary,
  $this: Token.ThisScope,
  $parent: Token.ParentScope,
  in: Token.InKeyword,
  instanceof: Token.InstanceOfKeyword,
  typeof: Token.TypeofKeyword,
  void: Token.VoidKeyword,
  of: Token.OfKeyword,
});

// Character scanning function lookup
const {
  CharScanners,
  IdParts,
} = /*@__PURE__*/ (() => {
  type CharScanner = (() => Token | null) & { notMapped?: boolean };

  const unexpectedCharacter: CharScanner = () => {
    throw createMappedError(ErrorNames.parse_unexpected_character, $input);
  };
  unexpectedCharacter.notMapped = true;

  /**
   * Ranges of code points in pairs of 2 (eg 0x41-0x5B, 0x61-0x7B, ...) where the second value is not inclusive (5-7 means 5 and 6)
   * Single values are denoted by the second value being a 0
   *
   * Copied from output generated with "node build/generate-unicode.js"
   *
   * See also: https://en.wikibooks.org/wiki/Unicode/Character_reference/0000-0FFF
   */
  const codes = {
    /* [$0-9A-Za_a-z] */
    AsciiIdPart: [0x24, 0, 0x30, 0x3A, 0x41, 0x5B, 0x5F, 0, 0x61, 0x7B],
    IdStart: /* IdentifierStart */[0x24, 0, 0x41, 0x5B, 0x5F, 0, 0x61, 0x7B, 0xAA, 0, 0xBA, 0, 0xC0, 0xD7, 0xD8, 0xF7, 0xF8, 0x2B9, 0x2E0, 0x2E5, 0x1D00, 0x1D26, 0x1D2C, 0x1D5D, 0x1D62, 0x1D66, 0x1D6B, 0x1D78, 0x1D79, 0x1DBF, 0x1E00, 0x1F00, 0x2071, 0, 0x207F, 0, 0x2090, 0x209D, 0x212A, 0x212C, 0x2132, 0, 0x214E, 0, 0x2160, 0x2189, 0x2C60, 0x2C80, 0xA722, 0xA788, 0xA78B, 0xA7AF, 0xA7B0, 0xA7B8, 0xA7F7, 0xA800, 0xAB30, 0xAB5B, 0xAB5C, 0xAB65, 0xFB00, 0xFB07, 0xFF21, 0xFF3B, 0xFF41, 0xFF5B],
    Digit: /* DecimalNumber */[0x30, 0x3A],
    Skip: /* Skippable */[0, 0x21, 0x7F, 0xA1]
  };

  /**
   * Decompress the ranges into an array of numbers so that the char code
   * can be used as an index to the lookup
   */
  const decompress = (lookup: (CharScanner | number)[] | null, $set: Set<number> | null, compressed: number[], value: CharScanner | number | boolean): void => {
    const rangeCount = compressed.length;
    for (let i = 0; i < rangeCount; i += 2) {
      const start = compressed[i];
      let end = compressed[i + 1];
      end = end > 0 ? end : start + 1;
      if (lookup) {
        lookup.fill(value as CharScanner | number, start, end);
      }
      if ($set) {
        for (let ch = start; ch < end; ch++) {
          $set.add(ch);
        }
      }
    }
  };

  // // ASCII IdentifierPart lookup
  // const AsciiIdParts = ((AsciiIdParts) => {
  //   decompress(null, AsciiIdParts, codes.AsciiIdPart, true);
  //   return AsciiIdParts;
  // })(new Set<number>());

  // IdentifierPart lookup
  const IdParts = /*@__PURE__*/ ((IdParts) => {
    decompress(IdParts as any, null, codes.IdStart, 1);
    decompress(IdParts as any, null, codes.Digit, 1);
    return IdParts;
  })(new Uint8Array(0xFFFF));

  // CharFuncLookup functions
  const returnToken = (token: Token): () => Token =>
    () => {
      nextChar();
      return token;
    };

  const CharScanners = new Array<CharScanner>(0xFFFF);
  CharScanners.fill(unexpectedCharacter, 0, 0xFFFF);

  decompress(CharScanners, null, codes.Skip, () => {
    nextChar();
    return null;
  });
  decompress(CharScanners, null, codes.IdStart, scanIdentifier);
  decompress(CharScanners, null, codes.Digit, () => scanNumber(false));

  CharScanners[Char.DoubleQuote] =
  CharScanners[Char.SingleQuote] = () => {
    return scanString();
  };
  CharScanners[Char.Backtick] = () => {
    return scanTemplate();
  };

  // !, !=, !==
  CharScanners[Char.Exclamation] = () => {
    if (nextChar() !== Char.Equals) {
      return Token.Exclamation;
    }
    if (nextChar() !== Char.Equals) {
      return Token.ExclamationEquals;
    }
    nextChar();
    return Token.ExclamationEqualsEquals;
  };

  // =, ==, ===, =>
  CharScanners[Char.Equals] =  () => {
    if (nextChar() === Char.GreaterThan) {
      nextChar();
      return Token.Arrow;
    }
    if ($currentChar !== Char.Equals) {
      return Token.Equals;
    }
    if (nextChar() !== Char.Equals) {
      return Token.EqualsEquals;
    }
    nextChar();
    return Token.EqualsEqualsEquals;
  };

  // &, &&
  CharScanners[Char.Ampersand] = () => {
    if (nextChar() !== Char.Ampersand) {
      return Token.Ampersand;
    }
    nextChar();
    return Token.AmpersandAmpersand;
  };

  // |, ||
  CharScanners[Char.Bar] = () => {
    if (nextChar() !== Char.Bar) {
      return Token.Bar;
    }
    nextChar();
    return Token.BarBar;
  };

  // ?, ??, ?.
  CharScanners[Char.Question] = () => {
    if (nextChar() === Char.Dot) {
      const peek = $charCodeAt($index + 1);
      if (peek <= Char.Zero || peek >= Char.Nine) {
        nextChar();
        return Token.QuestionDot;
      }
      return Token.Question;
    }
    if ($currentChar !== Char.Question) {
      return Token.Question;
    }
    nextChar();
    return Token.QuestionQuestion;
  };

  // ., ...
  CharScanners[Char.Dot] = () => {
    if (nextChar() <= Char.Nine && $currentChar >= Char.Zero) {
      return scanNumber(true);
    }
    if ($currentChar === Char.Dot) {
      if (nextChar() !== Char.Dot) {
        return Token.DotDot;
      }
      nextChar();
      return Token.DotDotDot;
    }
    return Token.Dot;
  };

  // <, <=
  CharScanners[Char.LessThan] =  () => {
    if (nextChar() !== Char.Equals) {
      return Token.LessThan;
    }
    nextChar();
    return Token.LessThanEquals;
  };

  // >, >=
  CharScanners[Char.GreaterThan] =  () => {
    if (nextChar() !== Char.Equals) {
      return Token.GreaterThan;
    }
    nextChar();
    return Token.GreaterThanEquals;
  };

  CharScanners[Char.Percent]      = returnToken(Token.Percent);
  CharScanners[Char.OpenParen]    = returnToken(Token.OpenParen);
  CharScanners[Char.CloseParen]   = returnToken(Token.CloseParen);

  // *, *=, **
  CharScanners[Char.Asterisk] =  () => {
    if (nextChar() === Char.Equals) {
      nextChar();
      return Token.AsteriskEquals;
    }
    if ($currentChar === Char.Asterisk) {
      nextChar();
      return Token.AsteriskAsterisk;
    }
    return Token.Asterisk;
  };

  // +, +=, ++
  CharScanners[Char.Plus] =  () => {
    if (nextChar() === Char.Plus) {
      nextChar();
      return Token.PlusPlus;
    }
    if ($currentChar !== Char.Equals) {
      return Token.Plus;
    }
    nextChar();
    return Token.PlusEquals;
  };

  CharScanners[Char.Comma]        = returnToken(Token.Comma);

  // -, -=, --
  CharScanners[Char.Minus] =  () => {
    if (nextChar() === Char.Minus) {
      nextChar();
      return Token.MinusMinus;
    }
    if ($currentChar !== Char.Equals) {
      return Token.Minus;
    }
    nextChar();
    return Token.MinusEquals;
  };

  // /, /=
  CharScanners[Char.Slash] =  () => {
    if (nextChar() !== Char.Equals) {
      return Token.Slash;
    }
    nextChar();
    return Token.SlashEquals;
  };

  CharScanners[Char.Colon]        = returnToken(Token.Colon);
  CharScanners[Char.Semicolon]    = returnToken(Token.Semicolon);
  CharScanners[Char.OpenBracket]  = returnToken(Token.OpenBracket);
  CharScanners[Char.CloseBracket] = returnToken(Token.CloseBracket);
  CharScanners[Char.OpenBrace]    = returnToken(Token.OpenBrace);
  CharScanners[Char.CloseBrace]   = returnToken(Token.CloseBrace);

  return { CharScanners, IdParts };
})();
